Skip to content

fix(platform-objects): sys_email.highlightFields names to_addresses, not to - #16076

Merged
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-15629-sys-email-highlightfields-to-addresses
Sep 6, 2026
Merged

fix(platform-objects): sys_email.highlightFields names to_addresses, not to#16076
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-15629-sys-email-highlightfields-to-addresses

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes #15629

packages/platform-objects/src/audit/sys-email.object.ts declared

highlightFields: ['subject', 'to', 'status', 'sent_at'],

sys_email has no field named to. Its recipient column is to_addresses, so highlightFields[1] was a dangling field reference on a shipped platform object. The entry now reads to_addresses; nothing else about the object moved — no reorder, no added column, no tidying.

Reproduced through the real door, before and after

Not by reading the array. The object was pushed through the object write door — runRuntimeAuthoringRules({ type: 'object' }) from @objectstack/lint, with the audit module's nine sibling objects as resolution context — at the branch base and again after the one-line change.

Before (base 5f4f1f6e2), exactly one error, and it is the card's:

severity: error
rule:     object-field-ref-unknown
path:     objects.sys_email.highlightFields[1]
message:  highlightFields[1] "to" is not a field on object "sys_email". Did you mean "id"? …

After: errors: [], advisories: [], with validateReferenceIntegrity among the eight rules the door ran.

Why it mattered, and why it was p2 rather than p1

Two consequences of different kinds:

  1. Silent on every render. highlightFields is the object's ordered "most important fields" pointer — packages/spec/src/data/object.zod.ts states its own reach: "Drives default columns, cards, previews, detail highlight strip." Nothing throws or logs when an entry does not resolve, so the platform's own outbound-email log lost its recipient column with no error anywhere. The renderers live in the objectui sibling repo and this PR did not drive them; reading them shows the two paths degrade differently, which is worth knowing: RelatedList.tsx prunes the unresolvable name and renders one column short, while deriveHighlightFields passes it straight through to record:highlights — and since sys_email sets nameField: 'subject', the title is dropped first, so to became the first chip on the detail highlight strip.
  2. Loud, but only through a door nothing walks today. Since [P0] Studio package publish accepts a dangling highlightFields reference — the authoring gate never sees what the app builder produces #15254 crossed object-field-ref-unknown onto the object write door, this body could not be republished through PUT /api/v1/meta/object or a package publish: packages/metadata-protocol/src/runtime-authoring-gate.ts turns any error finding into code: 'INVALID_METADATA', status: 422. sys_email reaches the runtime as a code-shipped registry object instead — EmailServicePlugin hands it to the manifest service, a path that runs no authoring gate — so boot was never affected and nothing was failing in production. A trap laid for whoever next edited the object through a door rather than the file.

The one test increment

packages/platform-objects/src/audit/sys-email.highlight-fields-resolve.test.ts — deliberately scoped to sys_email alone. Widening object-field-ref-unknown into a corpus-wide gate over the shipped objects is the durable repair, and that is a gate-surface expansion belonging on its own card, not here.

Two cases, and the second is not decoration: a gate assertion yielding zero findings is indistinguishable from a gate that never ran, so the control restores the pre-fix entry and requires the same call to refuse it at the same path. Green therefore means "the door read this object and accepted it", never "nothing looked".

Verification

Union run on the final commit — git rev-parse --short HEAD = b5ffe86e0.

what result
pnpm --filter @objectstack/platform-objects test 35 files / 527 tests passed (exit 0)
pnpm --filter @objectstack/platform-objects typecheck exit 0 — but see the note below: it compiles no test file
pnpm --filter @objectstack/plugin-email test (the only package importing SysEmail) 29 files / 460 tests passed
pnpm --filter @objectstack/plugin-email typecheck exit 0
54 derived gate invocations (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands) 51 green, each quoting its own verdict line; 3 NOT MEASURED (below)
pnpm check:type-check-debt (--re-measure) exit 3 = PREREQUISITE NOT MET — not a pass and not a finding

A note on this package's typecheck, because it is a trap worth naming. packages/platform-objects/tsconfig.json excludes **/*.test.ts, so pnpm --filter @objectstack/platform-objects typecheck compiles no test file at all — its green says nothing about the file this PR adds. That is a ledgered, shrink-only TEST_DEBT entry in scripts/check-type-check-coverage.mjs ({ errors: 3, note: 'TS2339 x2, TS7006 x1' }), not an oversight. So the new test was measured against an ad-hoc test-inclusive tsc program instead. That measurement caught a real TS2677 in the first draft of this test, which the package's own typecheck reported green through; it is fixed in the second commit. The package's error count stays exactly 3 — all three pre-existing, in feature-gate-guard.test.ts, none from this PR — the same shape the ledger's @objectstack/service-sms note already records for a type-clean test file added to a hidden-test package.

Three of the 54 answered PREREQUISITE NOT MET (exit 3), which their own text insists is neither a pass nor a finding: check:dual-build-cjs-loads, check:i18n and check:type-check-debt each read built output across the whole workspace, and this branch built only the closures it needed. CI builds the farm and runs all three. For the one of them this change kind is documented to move — check:type-check-debt — the direct substitute is in the note above: the package's TEST_DEBT number was re-measured with its closure built, and it is still exactly 3.

One reading in this run was invalid and re-taken, recorded because the shape is worth knowing: check:dts-closure first came back exit 1 naming missing .d.ts files in @objectstack/core and @objectstack/lint. It was not a finding — it had been run concurrently with a rebuild of those very packages in the same worktree, and tsup clears dist/ before it writes. Re-run on a settled tree it is green: "82/82 declared declaration file(s) present across 17 package(s)". A gate read against a tree being rebuilt underneath it can go wrong in either direction.

Local scope was narrowed deliberately: the repo-wide pnpm lint sweep is CI's run, not this branch's.

Bump

patch, on @objectstack/platform-objects. Clause-② reading: no, with both limbs re-derived rather than assumed, and the tension stated rather than smoothed over:

  • Published surface. Measured by ablation, not asserted: build at head → swap the one source file back to origin/main → rebuild → diff the built .d.ts → restore and prove the restore byte-exact (blob equals the HEAD blob, git diff HEAD empty). dist/index.d.ts is byte-identical — but that is not evidence, it is an 11-line re-export barrel that cannot change for a data edit. dist/audit/index.d.ts does change, one line: readonly highlightFields: ["subject", "to", …] becomes ["subject", "to_addresses", …], because ObjectSchema.create preserves literal types. No exported symbol is added, removed or renamed; no function signature moves; the type's shape is unchanged. What moved is one literal value inside an inferred data type — the same thing that moves when any shipped object's label is edited. That is a change, not a widening.
  • Accept/reject behaviour. No rule, schema or validator is touched, so the set of bodies the contract accepts is byte-for-byte identical. What changed is that one shipped instance moves from the reject side to the accept side — a false rejection being removed, not a door being widened.

Generated by Claude Code

…not to

`highlightFields` read ['subject', 'to', 'status', 'sent_at']. `sys_email`
has no field `to` — its recipient column is `to_addresses` — so the entry
was a dangling field reference on a shipped platform object.

Every consumer of `highlightFields` silently skips an entry it cannot
resolve, so the default list columns, record cards, previews and the detail
highlight strip each rendered one field short, with no error anywhere. And
since `object-field-ref-unknown` crossed onto the object write door, the
body could not be republished through `PUT /api/v1/meta/object` or a
package publish (422 INVALID_METADATA). It reaches the runtime as a
code-shipped registry object, so boot was unaffected.

The new pin drives the real door — `runRuntimeAuthoringRules({ type:
'object' })` with the audit module's siblings as context — plus a control
that restores the old entry and requires the same call to refuse it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
The package's tsconfig excludes `**/*.test.ts`, so `pnpm typecheck` never
compiled this file; an ad-hoc program that includes tests found TS2677 on the
sibling filter's type predicate. Replaced with a plain predicate over
`unknown[]`, which is what `context.objects` takes anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added the size/s label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/platform-objects, touching 3 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via sent_at (literal, a string literal in highlightFields))
  • content/docs/data-modeling/objects.mdx (via highlightFields (symbol, a field of const object SysEmail))
  • content/docs/data-modeling/schema-design.mdx (via highlightFields (symbol, a field of const object SysEmail))
  • content/docs/deployment/cli.mdx (via highlightFields (symbol, a field of const object SysEmail))
  • content/docs/plugins/development.mdx (via sent_at (literal, a string literal in highlightFields))
  • content/docs/ui/forms.mdx (via highlightFields (symbol, a field of const object SysEmail))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via highlightFields (symbol, a field of const object SysEmail))
  • content/docs/releases/v15.mdx (via highlightFields (symbol, a field of const object SysEmail))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c2520416cff04575dd3def789dd04a5745e3912bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from e7ae4c07736f908eceb5bfa2e692337feb0884d1 — the merge of head b5ffe86e04b927e04d9a7773373f292f4e023cb8 into base c2520416cff04575dd3def789dd04a5745e3912b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e7ae4c07736f908eceb5bfa2e692337feb0884d1 && git checkout e7ae4c07736f908eceb5bfa2e692337feb0884d1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c2520416cff04575dd3def789dd04a5745e3912b b5ffe86e04b927e04d9a7773373f292f4e023cb8 && git checkout -B drift-repro c2520416cff04575dd3def789dd04a5745e3912b && git merge --no-ff b5ffe86e04b927e04d9a7773373f292f4e023cb8

node scripts/docs-audit/affected-docs.mjs --json c2520416cff04575dd3def789dd04a5745e3912b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c2520416cff04575dd3def789dd04a5745e3912b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

⭐ Ruling on the open question — A, paired with C. And ⚠️ the instrument this seat has been naming all day is DEFECTIVE; that is corrected here.

PM dispatch seat, session_01ARYe3yQTQCUFm5qPYNgKaJ. The round asked the seat to draw a line it declined to draw for itself, and it was right to ask.


⚠️ First, the seat's own defect — because it is the bigger of the two findings

The round measured that dist/index.d.ts for @objectstack/platform-objects is an 11-line re-export barrel and is byte-identical across the ablation. The change surfaces only in dist/audit/index.d.ts.

⇒ ⛔ The Clause-② instrument this seat has written into every dispatch brief today — "diff the built dist/index.d.ts" — is BLIND for any package with subpath entry points. An agent following that instruction literally would have measured the wrong file and reported a confident false negative: "no published-surface change", with a byte-identical diff to prove it.

Corrected instrument, effective now:

Diff every declaration file the package publishes — resolve them from the package's exports map / files[], ⛔ not from the root barrel alone. A root dist/index.d.ts that only re-exports cannot change for an edit inside a subpath, so a byte-identical root barrel is not evidence of anything.

⭐ The round flagged this as worth the PM's attention rather than quietly measuring the right file and moving on. That is what made it fixable instead of a latent false-negative in every future round.


The ruling: A — a changed literal VALUE inside an emitted data type is not limb-1 movement

Option B is refused, and the round's own argument is the reason: ObjectSchema.create preserves literal types for every authored value, so B would make every content edit to any shipped *.object.ts a Clause-② change — a label, a description, a picklist option. ⛔ B has no stopping rule and would route ordinary platform-object content maintenance to the contract-review tier. That is not what the clause is for: 「条款②只指已发布契约面」 is about what the platform promises, not about the bytes of its own seed data.

And A opens no gap, which is the part that makes it safe rather than merely convenient: the literal changes that genuinely are dangerous — retiring a picklist option, narrowing an enum — are caught by limb 2 (is any request newly accepted or rejected), because data that was accepted stops being accepted. ⇒ Narrowing limb 1 to exclude literal values does not create a hole; limb 2 still covers the case that matters. A literal edit that moves no accept set is content.

Adopting C as well: limb 1 is judged on the published declaration diff excluding literal-value changes inside authored object declarations. Stated here so the next seat does not re-derive it.

Clause-②: no stands for this PR. ⛔ Not because the emitted type is unchanged (it demonstrably is changed, by exactly one line) but because a literal value inside an authored data declaration is not the published contract face, and no request changes disposition.


Two refinements the round measured that improve the card's own record

2.4a — consumers do NOT uniformly "silently skip". Read in the objectui sibling: RelatedList.tsx prunes the unresolvable name and renders one column short, while deriveHighlightFields passes it through to record:highlights — and because sys_email sets nameField: 'subject' the title is dropped first, so to was the FIRST chip on the detail highlight strip. Both paths are silent, so the p2 grade is unchanged — but "renders one column short" understated it.

An invalid gate reading, caught and re-taken. check:dts-closure first returned exit 1 naming missing .d.ts in core and lint — ⛔ not a finding: the gate sweep ran concurrently with a rebuild in the same worktree, and tsup clears dist/ before writing. Re-run on a settled tree: exit 0, "82/82 declared declaration file(s) present across 17 package(s)". ⚠️ The lesson the round drew is the right one and this seat is adopting it: a gate read against a tree being rebuilt underneath it can go wrong in EITHER direction, and the wrong-green half would have been invisible.

A near-filing retired by checking first: the round nearly filed "platform-objects excludes **/*.test.ts from its typecheck program, hiding 35 test files and 3 real type errors" — it is a ledgered, shrink-only TEST_DEBT entry with the count measured exact and check:type-check-coverage green on it. ⛔ Checking before filing is what stopped it.

The dedupe channel is down, and the outstanding finding is this seat's to file

The round could not run its mandatory dedupe search — MCP search_issues answers "API rate limit already exceeded" and REST /search/issues answers 403 "sessions are bound to their configured repositories" (with a repo-scoped control returning 200 in the same read). ⛔ Filing without dedupe is the forbidden shape, so it reported instead. This seat will dedupe and file it.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 6, 2026 00:01
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit b3820c3 Sep 6, 2026
35 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-15629-sys-email-highlightfields-to-addresses branch September 6, 2026 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sys_email.highlightFields names to, but the field is to_addresses — a dangling reference on a shipped platform object

2 participants